import contextlib
from pyson.json_encoding import register_cls

class RawModifyObserver(type):
    '''Raw observer interface for sequence of mapping containers: call 
    appropriate methods when user tries to modify object.
    '''
    def __new__(cls, name, bases, namespace):
        names = cls.get_methods(bases[0])
        names.update(namespace)
        return type.__new__(cls, name, bases, names)

    @staticmethod
    def init(obj, super_cls):
        obj.__raw_observer_super__ = super_cls

    @classmethod
    def get_methods(cls, super): #@ReservedAssignment
        def _mutate_size_action(self, method, *args):
            return self._mutate_both_action(method, *args)

        def _mutate_data_action(self, method, *args):
            return self._mutate_both_action(method, *args)

        def _mutate_both_action(self, method, *args):
            return method(self, *args)

        def __delitem__(self, idx):
            self._mutate_both_action(super.__delitem__, idx)

        def __setitem__(self, idx):
            self._mutate_data_action(super.__setitem__, idx)

        return dict(locals())

class RawSeqModifyObserver(RawModifyObserver):
    @classmethod
    def get_methods(cls, super): #@ReservedAssignment
        def __delslice__(self, i, j):
            return self._mutate_both_action(super.__delslice__, i, j)

        def __setslice__(self, i, j):
            return self._mutate_data_action(super.__setslice__, i, j)

        def append(self, obj):
            self._mutate_size_action(super.append, obj)

        def extend(self, iterable):
            self._mutate_size_action(super.extend, iterable)

        def insert(self, iterable):
            self._mutate_both_action(super.insert, iterable)

        def pop(self, *args):
            return self._mutate_both_action(super.pop, *args)

        def remove(self, value):
            self._mutate_both_action(super.remove, value)

        def reverse(self):
            self._mutate_data_action(super.reverse)

        def sort(self, cmp=None, key=None, reverse=False): #@ReservedAssignment
            self._mutate_data_action(super.extend, cmp=None, key=None, reverse=False) #@ReservedAssignment

        def __iadd__(self, value):
            self._mutate_size_action(super.__iadd__, value)

        def __imul__(self, value):
            self._mutate_size_action(super.__imul__, value)

        methods = RawModifyObserver.get_methods(super)
        methods.update(locals())
        del methods['methods']
        return methods

#===============================================================================
# Locked base containers
#===============================================================================
class Snapshot(object):
    def __repr__(self):
        return 'snapshot(%s)' % id(self)

    def __hash__(self):
        return id(self)

@register_cls('tutor.obslist')
class obslist(list):
    '''Observe list for changes.
    
    Users of ``obslist`` objects can monitor if the list has changed from a 
    given snapshot.
    
    >>> L = obslist([1, 2, 3, 4])
    >>> key1 = L.snapshot()
    >>> L.append(5)
    >>> key2 = L.snapshot()
    >>> L.changed(key1), L.changed(key2)
    (True, False)
    
    It is also possible to disable tracking of all changes inside a block
    using the L.not_tracking(snapshot) method in a with block:
    
    >>> with L.not_tracking(key1, key2):
    ...     L.append(6)
    >>> L.changed(key1), L.changed(key2)
    (True, False)
    
    Besides of these features, ``obslists`` behave like common lists
    
    >>> isinstance(L, list), L.pop()
    (True, 6)
    >>> L * 2
    [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
    '''
    __metaclass__ = RawSeqModifyObserver

    def __init__(self, *args):
        list.__init__(self, *args)
        self._snapshots = set([None])

    def snapshot(self):
        '''Returns a snapshot key. Use this key as argument in L.changed(key)
        to check if the list has changed since the given snapshot.'''

        key = Snapshot()
        self._snapshots.add(key)
        return key

    def changed(self, snapshot=None):
        '''Checks if the list has changed since the given snapshot. If no
        snapshot is given, checks if the list was changed since its creation.
        '''

        return snapshot not in self._snapshots

    def assure_unchanged(self, snapshot=None):
        '''Raises a ValueError if list has changed since the given snapshot.
        '''

        if self.changed(snapshot):
            raise ValueError('list has changed since last snapshot')

    def _mutate_both_action(self, method, *args):
        self._snapshots.clear()
        return method(self, *args)

    @contextlib.contextmanager
    def not_tracking(self, *snapshots):
        '''Temporarily disable modification checks for the given snapshots.
        '''

        good_snapshots = self._snapshots.intersection(snapshots)
        try:
            yield
        finally:
            self._snapshots.update(good_snapshots)

    # JSON encoding ------------------------------------------------------------
    def __json_encode__(self):
        return list(self)

    @classmethod
    def __json_decode__(cls, json):
        cls(json)

if __name__ == '__main__':
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)


